package com.gitee.wsl.data.state.repo

import com.gitee.wsl.data.state.KalugaState
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlin.coroutines.CoroutineContext

/**
 * A [BaseColdStateRepo] based on a [MutableStateFlow].
 *
 * This implementation allows for full fledged state transitions when the repo gains it's first subscriber
 * or loses all of it's subscribers.
 *
 * It also has an optional initial state.
 *
 * @param State the type of [KalugaState] represented by this repo.
 * @param coroutineContext the [CoroutineContext] used to create a coroutine scope for this state machine. Make sure that if you pass a coroutine context that has sequential execution if you do not want simultaneous state changes. The default Main dispatcher meets these criteria.
 * @param initChangeStateWithRepo method called when the repo transitions from no subscriptions to being subscribed to. Contains the last known [State] and provides a reference to this [ColdStateFlowRepo], and should result in a state transition.
 * @param deinitChangeStateWithRepo method called when the repo transitions from being subscribed to to no subscriptions. Contains the last known [State] and provides a reference to this [ColdStateFlowRepo], and allows to transition into a deinitialized state.
 * @param firstState Creates the first [State] when observation first takes place.
 */
open class ColdStateFlowRepo<State : KalugaState>(
    coroutineContext: CoroutineContext = Dispatchers.Main.immediate,
    val initChangeStateWithRepo: suspend (State, ColdStateFlowRepo<State>) -> (suspend () -> State),
    val deinitChangeStateWithRepo: suspend (State, ColdStateFlowRepo<State>) -> (suspend () -> State)?,
    val firstState: () -> State,
) : BaseColdStateRepo<State, MutableStateFlow<State>>(
    context = coroutineContext,
), StateFlowRepo<State> {

    /**
     * Constructor
     * @param coroutineContext the [CoroutineContext] used to create a coroutine scope for this state machine. Make sure that if you pass a coroutine context that has sequential execution if you do not want simultaneous state changes. The default Main dispatcher meets these criteria.
     * @param firstState Creates the first [State] when observation first takes place.
     * @param initChangeState method called when the repo transitions from no subscriptions to being subscribed to. Contains the last known [State] and should result in a state transition.
     * @param deinitChangeState method called when the repo transitions from being subscribed to to no subscriptions. Contains the last known [State] and should result in a state transition.
     */
    constructor(
        coroutineContext: CoroutineContext = Dispatchers.Main.immediate,
        // order is different than below because here firstState is mandatory, and to avoid JVM signature clashes
        firstState: () -> State,
        initChangeState: suspend (State) -> (suspend () -> State),
        deinitChangeState: suspend (State) -> (suspend () -> State),
    ) : this(
        coroutineContext,
        initChangeStateWithRepo = { state, _ -> initChangeState(state) },
        deinitChangeStateWithRepo = { state, _ -> deinitChangeState(state) },
        firstState = firstState,
    )

    /**
     * Constructor
     * @param coroutineContext the [CoroutineContext] used to create a coroutine scope for this state machine. Make sure that if you pass a coroutine context that has sequential execution if you do not want simultaneous state changes. The default Main dispatcher meets these criteria.
     * @param init method called when the repo transitions from no subscriptions to being subscribed to. Contains a reference to the [ColdStateFlowRepo] and should result in a [State] to transition to.
     * @param deinit method called when the repo transitions from being subscribed to to no subscriptions. Contains a reference to the [ColdStateFlowRepo] and should result in a [State] to transition to.
     * @param firstState Creates the first [State] when observation first takes place.
     * */
    constructor(
        coroutineContext: CoroutineContext = Dispatchers.Main.immediate,
        init: suspend (ColdStateFlowRepo<State>) -> State,
        deinit: suspend (ColdStateFlowRepo<State>) -> State?,
        firstState: () -> State,
    ) : this(
        coroutineContext,
        initChangeStateWithRepo = { _, repo -> { init(repo) } },
        deinitChangeStateWithRepo = { state, repo -> { deinit(repo) ?: state } },
        firstState = firstState,
    )

    override val stateFlow: StateFlow<State>
        get() = mutableFlow.asStateFlow()

    // the first initialization is done in the lazy block below since StateFlow requires an initial value
    final override suspend fun initialValue(): State = mutableFlow.value

    final override suspend fun noMoreCollections() = takeAndChangeState { state ->
        deinitChangeStateWithRepo(state, this) ?: state.remain()
    }

    override val lazyMutableFlow: Lazy<MutableStateFlow<State>> =
        lazy {
            MutableStateFlow(
                firstState(),
            )
        }

    override suspend fun firstCollection() {
        laterCollections()
    }

    override suspend fun laterCollections() = takeAndChangeState { state ->
        initChangeStateWithRepo(state, this@ColdStateFlowRepo)
    }
}